/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.isis.viewer.wicket.ui.pages; import java.util.Arrays; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Locale; import com.google.inject.name.Named; import org.apache.wicket.Component; import org.apache.wicket.MarkupContainer; import org.apache.wicket.Page; import org.apache.wicket.RestartResponseAtInterceptPageException; import org.apache.wicket.behavior.Behavior; import org.apache.wicket.devutils.debugbar.DebugBar; import org.apache.wicket.devutils.debugbar.IDebugBarContributor; import org.apache.wicket.devutils.debugbar.InspectorDebugPanel; import org.apache.wicket.event.Broadcast; import org.apache.wicket.markup.head.CssHeaderItem; import org.apache.wicket.markup.head.CssReferenceHeaderItem; import org.apache.wicket.markup.head.IHeaderResponse; import org.apache.wicket.markup.head.JavaScriptHeaderItem; import org.apache.wicket.markup.head.JavaScriptReferenceHeaderItem; import org.apache.wicket.markup.head.OnDomReadyHeaderItem; import org.apache.wicket.markup.head.PriorityHeaderItem; import org.apache.wicket.markup.head.filter.HeaderResponseContainer; import org.apache.wicket.markup.html.WebMarkupContainer; import org.apache.wicket.markup.html.WebPage; import org.apache.wicket.markup.html.basic.Label; import org.apache.wicket.markup.html.panel.EmptyPanel; import org.apache.wicket.model.IModel; import org.apache.wicket.protocol.http.ClientProperties; import org.apache.wicket.protocol.http.WebSession; import org.apache.wicket.protocol.http.request.WebClientInfo; import org.apache.wicket.request.mapper.parameter.PageParameters; import org.apache.wicket.request.resource.CssResourceReference; import org.apache.wicket.request.resource.JavaScriptResourceReference; import org.apache.wicket.request.resource.PackageResource; import org.apache.wicket.request.resource.ResourceReference; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.isis.applib.services.exceprecog.ExceptionRecognizer; import org.apache.isis.applib.services.exceprecog.ExceptionRecognizerComposite; import org.apache.isis.core.commons.authentication.AuthenticationSession; import org.apache.isis.core.commons.config.IsisConfiguration; import org.apache.isis.core.metamodel.adapter.mgr.AdapterManager; import org.apache.isis.core.metamodel.services.ServicesInjector; import org.apache.isis.core.runtime.system.context.IsisContext; import org.apache.isis.core.runtime.system.persistence.PersistenceSession; import org.apache.isis.core.runtime.system.session.IsisSessionFactory; import org.apache.isis.viewer.wicket.model.common.PageParametersUtils; import org.apache.isis.viewer.wicket.model.hints.IsisEnvelopeEvent; import org.apache.isis.viewer.wicket.model.hints.IsisEventLetterAbstract; import org.apache.isis.viewer.wicket.model.hints.UiHintContainer; import org.apache.isis.viewer.wicket.model.models.ActionPrompt; import org.apache.isis.viewer.wicket.model.models.ActionPromptProvider; import org.apache.isis.viewer.wicket.model.models.BookmarkableModel; import org.apache.isis.viewer.wicket.model.models.BookmarkedPagesModel; import org.apache.isis.viewer.wicket.model.models.EntityModel; import org.apache.isis.viewer.wicket.model.models.PageType; import org.apache.isis.viewer.wicket.ui.ComponentFactory; import org.apache.isis.viewer.wicket.ui.ComponentType; import org.apache.isis.viewer.wicket.ui.app.registry.ComponentFactoryRegistry; import org.apache.isis.viewer.wicket.ui.app.registry.ComponentFactoryRegistryAccessor; import org.apache.isis.viewer.wicket.ui.components.actionprompt.ActionPromptModalWindow; import org.apache.isis.viewer.wicket.ui.components.widgets.favicon.Favicon; import org.apache.isis.viewer.wicket.ui.errors.ExceptionModel; import org.apache.isis.viewer.wicket.ui.errors.JGrowlBehaviour; import org.apache.isis.viewer.wicket.ui.util.CssClassAppender; import de.agilecoders.wicket.core.Bootstrap; import de.agilecoders.wicket.core.markup.html.references.BootlintHeaderItem; import de.agilecoders.wicket.core.markup.html.references.BootstrapJavaScriptReference; import de.agilecoders.wicket.core.settings.IBootstrapSettings; import de.agilecoders.wicket.core.settings.ITheme; import de.agilecoders.wicket.extensions.markup.html.bootstrap.icon.FontAwesomeCssReference; /** * Convenience adapter for {@link WebPage}s built up using {@link ComponentType}s. */ public abstract class PageAbstract extends WebPage implements ActionPromptProvider { private static Logger LOG = LoggerFactory.getLogger(PageAbstract.class); private static final long serialVersionUID = 1L; /** * @see <a href="http://github.com/brandonaaron/livequery">livequery</a> */ private static final JavaScriptResourceReference JQUERY_LIVEQUERY_JS = new JavaScriptResourceReference(PageAbstract.class, "jquery.livequery.js"); private static final JavaScriptResourceReference JQUERY_ISIS_WICKET_VIEWER_JS = new JavaScriptResourceReference(PageAbstract.class, "jquery.isis.wicket.viewer.js"); private static final String LIVE_RELOAD_URL_KEY = "isis.viewer.wicket.liveReloadUrl"; // not to be confused with the bootstrap theme... // is simply a CSS class derived from the application's name private static final String ID_THEME = "theme"; private static final String ID_BOOKMARKED_PAGES = "bookmarks"; private static final String ID_ACTION_PROMPT_MODAL_WINDOW = "actionPromptModalWindow"; private static final String ID_PAGE_TITLE = "pageTitle"; private static final String ID_FAVICON = "favicon"; public static final String ID_MENU_LINK = "menuLink"; public static final String UIHINT_FOCUS = "focus"; /** * This is a bit hacky, but best way I've found to pass an exception over to the WicketSignInPage * if there is a problem rendering this page. */ public static ThreadLocal<ExceptionModel> EXCEPTION = new ThreadLocal<>(); private final List<ComponentType> childComponentIds; /** * {@link com.google.inject.Inject Inject}ed when {@link #init() initialized}. */ @com.google.inject.Inject @Named("applicationName") private String applicationName; /** * {@link com.google.inject.Inject Inject}ed when {@link #init() initialized}. */ @com.google.inject.Inject(optional = true) @Named("applicationCss") private String applicationCss; /** * {@link com.google.inject.Inject Inject}ed when {@link #init() initialized}. */// @com.google.inject.Inject(optional = true) @Named("applicationJs") private String applicationJs; /** * {@link com.google.inject.Inject Inject}ed when {@link #init() initialized}. */ @com.google.inject.Inject private PageClassRegistry pageClassRegistry; /** * Top-level <div> to which all content is added. * * <p> * Has <code>protected</code> visibility so that subclasses can also add directly to this div. * </p> */ protected MarkupContainer themeDiv; public PageAbstract( final PageParameters pageParameters, final String title, final ComponentType... childComponentIds) { super(pageParameters); try { // for breadcrumbs support getSession().bind(); setTitle(title); add(new Favicon(ID_FAVICON)); themeDiv = new WebMarkupContainer(ID_THEME); add(themeDiv); if(applicationName != null) { themeDiv.add(new CssClassAppender(CssClassAppender.asCssStyle(applicationName))); } DebugBar debugBar = null; if (getApplication().getDebugSettings().isDevelopmentUtilitiesEnabled()) { debugBar = newDebugBar("debugBar"); } if (debugBar != null) { add(debugBar); } else { add(new EmptyPanel("debugBar").setVisible(false)); } MarkupContainer header = createPageHeader("header"); themeDiv.add(header); MarkupContainer footer = createPageFooter("footer"); themeDiv.add(footer); addActionPromptModalWindow(themeDiv); this.childComponentIds = Collections.unmodifiableList(Arrays.asList(childComponentIds)); // ensure that all collected JavaScript contributions are loaded at the page footer add(new HeaderResponseContainer("footerJS", "footerJS")); } catch(final RuntimeException ex) { LOG.error("Failed to construct page, going back to sign in page", ex); // REVIEW: similar code in WebRequestCycleForIsis final List<ExceptionRecognizer> exceptionRecognizers = getServicesInjector().lookupServices(ExceptionRecognizer.class); final String recognizedMessageIfAny = new ExceptionRecognizerComposite(exceptionRecognizers).recognize(ex); final ExceptionModel exceptionModel = ExceptionModel.create(recognizedMessageIfAny, ex); getSession().invalidate(); getSession().clear(); // for the WicketSignInPage to render EXCEPTION.set(exceptionModel); throw new RestartResponseAtInterceptPageException(getSignInPage()); } } protected DebugBar newDebugBar(final String id) { final DebugBar debugBar = new DebugBar(id); final List<IDebugBarContributor> contributors = DebugBar.getContributors(getApplication()); for (Iterator<IDebugBarContributor> iterator = contributors.iterator(); iterator.hasNext(); ) { final IDebugBarContributor contributor = iterator.next(); // the InspectorDebug invokes load on every model found. // for ActionModels this has the rather unfortunate effect of invoking them! // https://issues.apache.org/jira/browse/ISIS-1622 raised to refactor and then reinstate this if(contributor == InspectorDebugPanel.DEBUG_BAR_CONTRIB) { iterator.remove(); } } return debugBar; } /** * Creates the component that should be used as a page header/navigation bar * * @param id The component id * @return The container that should be used as a page header/navigation bar */ protected MarkupContainer createPageHeader(final String id) { Component header = getComponentFactoryRegistry().createComponent(ComponentType.HEADER, id, null); return (MarkupContainer) header; } /** * Creates the component that should be used as a page header/navigation bar * * @param id The component id * @return The container that should be used as a page header/navigation bar */ protected MarkupContainer createPageFooter(final String id) { Component footer = getComponentFactoryRegistry().createComponent(ComponentType.FOOTER, id, null); return (MarkupContainer) footer; } protected void setTitle(final String title) { addOrReplace(new Label(ID_PAGE_TITLE, title != null? title: applicationName)); } private Class<? extends Page> getSignInPage() { return pageClassRegistry.getPageClass(PageType.SIGN_IN); } @Override public void renderHead(final IHeaderResponse response) { super.renderHead(response); response.render(new PriorityHeaderItem(JavaScriptHeaderItem.forReference(getApplication().getJavaScriptLibrarySettings().getJQueryReference()))); response.render(new PriorityHeaderItem(JavaScriptHeaderItem.forReference(BootstrapJavaScriptReference.instance()))); response.render(CssHeaderItem.forReference(FontAwesomeCssReference.instance())); response.render(CssHeaderItem.forReference(new BootstrapOverridesCssResourceReference())); contributeThemeSpecificOverrides(response); response.render(JavaScriptReferenceHeaderItem.forReference(JQUERY_LIVEQUERY_JS)); response.render(JavaScriptReferenceHeaderItem.forReference(JQUERY_ISIS_WICKET_VIEWER_JS)); final JGrowlBehaviour jGrowlBehaviour = new JGrowlBehaviour(); jGrowlBehaviour.renderFeedbackMessages(response); if(applicationCss != null) { response.render(CssReferenceHeaderItem.forUrl(applicationCss)); } if(applicationJs != null) { response.render(JavaScriptReferenceHeaderItem.forUrl(applicationJs)); } String liveReloadUrl = getConfiguration().getString(LIVE_RELOAD_URL_KEY); if(liveReloadUrl != null) { response.render(JavaScriptReferenceHeaderItem.forUrl(liveReloadUrl)); } if(isModernBrowser()) { addBootLint(response); } String markupId = null; UiHintContainer hintContainer = getUiHintContainerIfAny(); if(hintContainer != null) { String path = hintContainer.getHint(getPage(), PageAbstract.UIHINT_FOCUS); if(path != null) { Component childComponent = get(path); if(childComponent != null) { markupId = childComponent.getMarkupId(); } } } String javaScript = markupId != null ? String.format("Wicket.Event.publish(Isis.Topic.FOCUS_FIRST_PROPERTY, '%s')", markupId) : "Wicket.Event.publish(Isis.Topic.FOCUS_FIRST_PROPERTY)"; response.render(OnDomReadyHeaderItem.forScript(javaScript)); } protected UiHintContainer getUiHintContainerIfAny() { return null; } private void addBootLint(final IHeaderResponse response) { // rather than using the default BootlintHeaderItem.INSTANCE; // this allows us to assign 'form-control' class to an <a> (for x-editable styling) response.render(new BootlintHeaderItem("bootlint.showLintReportForCurrentDocument(['E042'], {'problemFree': false});")); } private boolean isModernBrowser() { return !isIePre9(); } private boolean isIePre9() { final WebClientInfo clientInfo = WebSession.get().getClientInfo(); final ClientProperties properties = clientInfo.getProperties(); if (properties.isBrowserInternetExplorer()) if (properties.getBrowserVersionMajor() < 9) return true; return false; } /** * Contributes theme specific Bootstrap CSS overrides if there is such resource * * @param response The header response to contribute to */ private void contributeThemeSpecificOverrides(final IHeaderResponse response) { final IBootstrapSettings bootstrapSettings = Bootstrap.getSettings(getApplication()); final ITheme activeTheme = bootstrapSettings.getActiveThemeProvider().getActiveTheme(); final String name = activeTheme.name().toLowerCase(Locale.ENGLISH); final String themeSpecificOverride = "bootstrap-overrides-" + name + ".css"; final ResourceReference.Key themeSpecificOverrideKey = new ResourceReference.Key(PageAbstract.class.getName(), themeSpecificOverride, null, null, null); if (PackageResource.exists(themeSpecificOverrideKey)) { response.render(CssHeaderItem.forReference(new CssResourceReference(themeSpecificOverrideKey))); } } /** * As provided in the {@link #PageAbstract(org.apache.wicket.request.mapper.parameter.PageParameters, String, org.apache.isis.viewer.wicket.ui.ComponentType...)} constructor}. * * <p> * This superclass doesn't do anything with this property directly, but * requiring it to be provided enforces standardization of the * implementation of the subclasses. */ public List<ComponentType> getChildModelTypes() { return childComponentIds; } /** * For subclasses to call. * * <p> * Should be called in the subclass' constructor. * * @param model * - used to find the best matching {@link ComponentFactory} to * render the model. */ protected void addChildComponents(final MarkupContainer container, final IModel<?> model) { for (final ComponentType componentType : getChildModelTypes()) { addComponent(container, componentType, model); } } private void addComponent(final MarkupContainer container, final ComponentType componentType, final IModel<?> model) { getComponentFactoryRegistry().addOrReplaceComponent(container, componentType, model); } //////////////////////////////////////////////////////////////// // bookmarked pages //////////////////////////////////////////////////////////////// /** * Convenience for subclasses */ protected void addBookmarkedPages(final MarkupContainer container) { Component bookmarks = getComponentFactoryRegistry().createComponent(ComponentType.BOOKMARKED_PAGES, ID_BOOKMARKED_PAGES, getBookmarkedPagesModel()); container.add(bookmarks); bookmarks.add(new Behavior() { @Override public void onConfigure(Component component) { super.onConfigure(component); PageParameters parameters = getPageParameters(); component.setVisible(parameters.get(PageParametersUtils.ISIS_NO_HEADER_PARAMETER_NAME).isNull()); } }); } protected void bookmarkPage(final BookmarkableModel<?> model) { getBookmarkedPagesModel().bookmarkPage(model); } protected void removeAnyBookmark(final EntityModel model) { getBookmarkedPagesModel().remove(model); } private BookmarkedPagesModel getBookmarkedPagesModel() { final BookmarkedPagesModelProvider session = (BookmarkedPagesModelProvider) getSession(); return session.getBookmarkedPagesModel(); } // /////////////////////////////////////////////////////////////////// // ActionPromptModalWindowProvider // /////////////////////////////////////////////////////////////////// private ActionPromptModalWindow actionPromptModalWindow; public ActionPrompt getActionPrompt() { return actionPromptModalWindow; } private void addActionPromptModalWindow(final MarkupContainer parent) { actionPromptModalWindow = ActionPromptModalWindow.newModalWindow(ID_ACTION_PROMPT_MODAL_WINDOW); parent.addOrReplace(actionPromptModalWindow); } // /////////////////////////////////////////////////////////////////// // UI Hint // /////////////////////////////////////////////////////////////////// /** * Propagates all {@link org.apache.isis.viewer.wicket.model.hints.IsisEventLetterAbstract letter} events down to * all child components, wrapped in an {@link org.apache.isis.viewer.wicket.model.hints.IsisEnvelopeEvent envelope} event. */ public void onEvent(final org.apache.wicket.event.IEvent<?> event) { final Object payload = event.getPayload(); if(payload instanceof IsisEventLetterAbstract) { final IsisEventLetterAbstract letter = (IsisEventLetterAbstract)payload; final IsisEnvelopeEvent broadcastEv = new IsisEnvelopeEvent(letter); send(this, Broadcast.BREADTH, broadcastEv); } } //region > getComponentFactoryRegistry (Convenience) protected ComponentFactoryRegistry getComponentFactoryRegistry() { final ComponentFactoryRegistryAccessor cfra = (ComponentFactoryRegistryAccessor) getApplication(); return cfra.getComponentFactoryRegistry(); } //endregion //region > injected (application-scope) // REVIEW: can't inject because not serializable. protected IsisSessionFactory getIsisSessionFactory() { return IsisContext.getSessionFactory(); } protected IsisConfiguration getConfiguration() { return getIsisSessionFactory().getConfiguration(); } protected ServicesInjector getServicesInjector() { return getIsisSessionFactory().getServicesInjector(); } //endregion //region > derived from injected components (session-scope) protected PersistenceSession getPersistenceSession() { return getIsisSessionFactory().getCurrentSession().getPersistenceSession(); } protected AuthenticationSession getAuthenticationSession() { return getIsisSessionFactory().getCurrentSession().getAuthenticationSession(); } //endregion }